/*
 * Copyright (c) 2009, Magnus Robertsson. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

package se.jayway.javafxbinder.infrastructure;

import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

import org.aspectj.lang.Signature;

/**
 * A generic aspect that introduce the methods for any class that is of type
 * Bean. To use the aspect you'll need to specify what classes should implement
 * the Bean interface. For example:
 * <pre>
   public aspect MyJavaBeans {
       declare parents: (se.jayway.javafxbinder.test.pojos.*) implements Bean;
   }
 * </pre>
 * This way you can easily specify the classes by using an annotation (@Bean).
 * 
 * @author Magnus Robertsson
 */
public aspect BeanAspect {
	pointcut setters(Bean bean) : execution(void *..set*(..)) && target(bean);

	void around(Bean bean) : setters(bean) {
		Signature signature = thisJoinPointStaticPart.getSignature();
        String propertyName = extractPropertyName(signature);
        Object oldValue = getPropertyValue(bean, signature);
        Object newValue = thisJoinPoint.getArgs()[0];
        proceed(bean);
        bean.firePropertyChange(bean, propertyName, oldValue, newValue);
	}

	private Object getPropertyValue(Bean bean, Signature signature) {
		try {
			return findGetterMethod(signature).invoke(bean);
		} catch (IllegalAccessException e) {
			throw new UnsupportedOperationException("Getter method need to be public", e);
		} catch (InvocationTargetException e) {
			throw new RuntimeException("Getter method throwed exception", e);
		}
	}

	private String extractPropertyName(Signature signature) {
		String setterName = signature.getName();
		if (setterName.length() > 4) {
			return Character.toLowerCase(setterName.charAt(3)) + setterName.substring(4);
		}
		return setterName.toLowerCase();
	}

	@SuppressWarnings("unchecked")
	private Method findGetterMethod(Signature signature) {
		String propertyName = signature.getName().substring(3);
		try {
			return signature.getDeclaringType().getMethod("get" + propertyName);
		} catch (NoSuchMethodException e1) {
			try {
				return signature.getDeclaringType().getMethod("is" + propertyName);
			} catch (NoSuchMethodException e2) {
				throw new UnsupportedOperationException("No getter method for property " + propertyName);
			}
		}
	}
	
    transient PropertyChangeSupport Bean.support = new PropertyChangeSupport(this);

    public void Bean.addPropertyChangeListener(PropertyChangeListener listener) {
        support.addPropertyChangeListener(listener);
    }

    public void Bean.addPropertyChangeListener(String propertyName, PropertyChangeListener listener) {
        support.addPropertyChangeListener(propertyName, listener);
    }

    public void Bean.removePropertyChangeListener(String propertyName, PropertyChangeListener listener) {
        support.removePropertyChangeListener(propertyName, listener);
    }

    public void Bean.removePropertyChangeListener(PropertyChangeListener listener) {
        support.removePropertyChangeListener(listener);
    }

    public boolean Bean.hasListeners(String propertyName) {
        return support.hasListeners(propertyName);
    }

    private void Bean.firePropertyChange(Bean bean,
                                    String propertyName,
                                    Object oldValue,
                                    Object newValue) {
        support.firePropertyChange(
                propertyName,
                oldValue,
                newValue);
    }
}
